<?php
require_once 'Zend/Loader.php';
Zend_Loader::loadClass('Zend_Gdata');
Zend_Loader::loadClass('Zend_Gdata_AuthSub');
Zend_Loader::loadClass('Zend_Gdata_ClientLogin');
Zend_Loader::loadClass('Zend_Gdata_Calendar');

/**
 * Google calendar mass import class.
 * 
 * @author Daniel Morlock <daniel.morlock@gmail.com>
 *
 */
class Import
{
    /**
     * Email reminder
     * @var const
     */
    const REMINDER_EMAIL = "email";
    
    /**
     * SMS reminder
     * @var const
     */
    const REMINDER_SMS = "sms";
    
    /**
     * Popup reminder
     * @var const
     */
    const REMINDER_POPUP = "alert";
    
    /**
     * Reminder 1 day before event.
     * @var const
     */
    const REMINDER_ONE_DAY = 1440;
    
    /**
     * Ignore conflicts of events
     * @var const
     */
    const CONFLICT_IGNORE = "ignore";
    
    /**
     * Shift conflicting events to resolve conflicts.
     * @var const
     */
    const CONFLICT_SHIFT = "shift";
    
    /**
     * Holds current reminders which are added to the corresponding events.
     * @var Zend_Gdata_Extension_Reminder[]
     */
    private $Reminders = array();

    /**
     * Holds current event filters in an array of tuples containing the
     * appropriate attribute and an expression that the attribute must
     * contain to be included.
     */
    private $Filters = array();
    
	/**
	 * Holds contents of xml file containing events to be imported.
	 * 
	 * @see Import::Parse() in order to fill database
	 * @var SimpleXMLElement
	 */
	public $Data;
		
	/**
	 * Timezone for the current dates.
	 * 
	 * @var String
	 */
	private $Timezone = '+02';
	
	/**
	 * Http client for Google service. 
	 * @var Zend_Gdata_HttpClient
	 */
	private $Client = null;
	
	/**
	 * Google calendar object where events will be added to.
	 * @var Zend_Gdata_Calendar
	 */
	private $Calendar = null;
	
	/**
	 * Contains template string for the event title. A template string can contain xpath 
	 * information quotes by html tags.
	 * @example <parent/children/attribute>
	 * @var String
	 */
	public $Title;
	
	/**
	 * Template string for event location. Can be null for no title.
	 * @var String
	 */
	public $Location = null;
	
	/**
	 * Template string representing the event content. Can be null for no content.
	 * @var String
	 */
	public $Content = null;
	
	/**
	 * Template string for event start date. This is expected to result in a valid
	 * date string which can be read from php class DateTime.
	 * 
	 * @link http://www.php.net/manual/en/datetime.construct.php
	 * @var String
	 */
	public $Start;
	
	/**
	 * Template string for event end date. This is expected to result in a valid
	 * date string which can be read from php class DateTime.
	 * 
	 * @link http://www.php.net/manual/en/datetime.construct.php
	 * @var String
	 * @see Import::$Duration
	 */
	public $End = null;
	
	/**
	 * Specified event duration in minutes. Either this or Import::$End
	 * must be specified.
	 * 
	 * @var int
	 * @see Import::$End
	 */
	public $Duration = null;
	
	/**
	 * Number of events just added to the calendar. 
	 * 
	 * @see Import::Add()
	 * @var int
	 */
	private $Number = 0;
	
	/**
	 * Contains unix timestamps of already added events. This is used to resolve
	 * conflicts if Import::CONFLICT_SHIFT was chosen.
	 * 
	 * @see Import::Add()
	 * @var int[]
	 */
	private $Timestamps = array();
	
	/**
	 * Defines a unique key template which is translated and used to detect conflicting events. 
	 * Here: Two events conflicts if the unique key is equal.
	 * 
	 * @see Import::Add()
	 * @var String
	 */
	public $UniqueKey = null;
	
	/**
	 * Constructore authenticates at the Google service.
	 * 
	 * @param String $Username
	 * @param String $Password
	 * @return void
	 */
	public function __construct($Username,$Password)
	{
		$this->Client = Zend_Gdata_ClientLogin::getHttpClient($Username,$Password,Zend_Gdata_Calendar::AUTH_SERVICE_NAME);
		$this->Calendar = new Zend_Gdata_Calendar($this->Client);
	}
	
	/**
	 * Reads the specified XML file using SimpleXML.
	 *  
	 * @param String $Path
	 * @return void
	 */
	public function Read($Path)
	{			
        $this->Data = simplexml_load_file($Path);
	}
		
	/**
	 * Registeres another reminder which will be added to the corresponding events.
	 * 
	 * @param String $Method = Import::REMINDER_EMAIL
	 * @param int $Minutes = Import::ONE_DAY Specifies minutes before event when reminder fires
	 * @return void
	 */
	public function AddReminder($Method = Import::REMINDER_EMAIL, $Minutes = Import::REMINDER_ONE_DAY)
	{
	    $Reminder = $this->Calendar->newReminder();
	    $Reminder->method = $Method;
	    
	    // Days ?
	    if($Minutes % 1440 == 0)
	        $Reminder->days =  $Minutes / 1440;
	        
	    // Hours?
	    elseif($Minutes % 60 == 0)
	        $Reminders->hours = $Minutes / 60;
	        
	    // Minutes!
	    else $Reminder->minutes = $Minutes;
	    
	    array_push($this->Reminders, $Reminder);
	}

    /**
     * Registeres another filter to exclude events that does not match this filter.
     * Filters should be applied to attributes "title", "where" and "content" of 
     * Zend_Gdata_Calendar_EventEntry.
     *
     * @param String $Attribute
     * @param String $Contains
     * @return void
     */
    public function AddFilter($Attribute, $Contains)
    {
        array_push($this->Filters, Array($Attribute, $Contains));
    }
		
	/**
	 * Creates Google events and adds them to the specified Google calendar. Returns
	 * the number of created events. 
	 * If there are conflicting events (events that start exactly at the same time), you 
	 * can specify $Conflicts = Import::CONFLICT_SHIFT in order to shift the latter event
	 * in order to resolve that conflict.
	 * 
	 * @param String $Feed Google calendar feed URL
	 * @param String $XPath xpath to the tokens to import
	 * @param String $Conflicts = Import::CONFLICT_IGNORE 
	 * @return int
	 */
	public function Add($Feed, $XPath = null, $Conflicts = Import::CONFLICT_IGNORE)
	{
	    $this->Number = 0;
	    foreach($this->Data->xpath($XPath) as $Event)
	    {	 
	    	$Start = new DateTime($this->Parse($this->Start));
		        
	        if($this->End != null)
	            $End = new DateTime($this->Parse($this->End));
	        else
	        {
	            $End = clone $Start;
	            $End->modify("+$this->Duration minutes");
	        }
	        
	        if($Conflicts == Import::CONFLICT_SHIFT)
	        {
       	        // Check for conflicts
       	        $Hash = $this->Parse($this->UniqueKey);
       	        if(isset($this->Timestamps[$Hash]))
       	        {
       	            $Duration = $this->Timestamps[$Hash] - $this->Timestamp($Start);
       	            $Start->modify("+$Duration seconds");
       	            $End->modify("+$Duration seconds");
       	        }
       	        $this->Timestamps[$Hash] = $this->Timestamp($End);
	        }
	        
	        $Date = $this->Calendar->newWhen();
	        $Date->startTime = $Start->format("Y-m-d")."T".$Start->format("H:i").":00.000{$this->Timezone}:00";
	        $Date->endTime = $End->format("Y-m-d")."T".$End->format("H:i").":00.000{$this->Timezone}:00";
	        $Date->reminders = $this->Reminders;

	        $Event = $this->Calendar->newEventEntry();
	        $Event->title = $this->Calendar->newTitle($this->Parse($this->Title));
	        $Event->where = array($this->Calendar->newWhere($this->Parse($this->Location)));
	        $Event->content = $this->Calendar->newContent($this->Parse($this->Content));
	        $Event->when = array($Date);

            $Include = true;
            foreach($this->Filters as $Filter)
            {
                list($Attr, $Contains) = $Filter;
                if(!isset($Event->$Attr)) continue;

                if(stristr($Event->$Attr, $Contains) === false)
                {
                    $Include = false;
                    break;
                }
            }

            if($Include)
	        {
                while(true)
                {
                    try
                    {
        	            $this->Calendar->insertEvent($Event,$Feed);
                        break;
                    }
                    catch(Zend_Gdata_App_HttpException $e)
                    {
                        echo "Got an HTTP exception: $e\n";
                        echo "Wait 10 seconds and re-try ...";
                        sleep(10);
                        echo "Re-trying ...";
                    }
                }
            }
	        $this->Number ++;
	    }
	    
	    return $this->Number + 1;
	}
	
	/**
	 * Returns a unix timestamp from the given date time object.
	 * 
	 * @param DateTime $Time
	 * @return int
	 */
	private function Timestamp($Time)
	{
	    return mktime($Time->format("G"), (int)$Time->format("i"), (int)$Time->format("s"), $Time->format("n"), $Time->format("j"), $Time->format("Y"));
	}
	
	/**
	 * Parses given template for xpathes within html tags and replaces them
	 * with the corresponding value the xpath references.
	 * 
	 * @param String $Template
	 * @return String
	 */
	private function Parse($Template)
	{
	    return preg_replace_callback('/<([^>]*)>/U',array($this, 'Replace'), $Template);
	}
	
	/**
	 * Callback handler for Import::Parse() method which is called foreach xpath
	 * found in the template. Method replaces xpath with corresponding value from current data.
	 * 
	 * @param String[] $Matches
	 * @return String
	 */
	private function Replace($Matches)
	{
	    $Value = $this->Data->xpath($Matches[1]);
	    
	    if($Value == false)
	        throw new Exception("xpath: ".$Matches[1]." not found");
	    
	    if(is_array($Value))
	    {
	        if(sizeof($Value) > 1) $Value = $Value[$this->Number];
	        else $Value = $Value[0];
	    }
	    
	    return (string)$Value;
	}
}

?>
